/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package org.apache.solr.schema;

import org.apache.lucene.index.AtomicReaderContext;
import org.apache.lucene.index.IndexableField;
import org.apache.lucene.queries.function.docvalues.DocTermsIndexDocValues;
import org.apache.lucene.search.Query;
import org.apache.lucene.search.SortField;
import org.apache.lucene.search.TermRangeQuery;
import org.apache.lucene.queries.function.FunctionValues;
import org.apache.lucene.queries.function.ValueSource;
import org.apache.lucene.queries.function.valuesource.FieldCacheSource;
import org.apache.lucene.util.BytesRef;
import org.apache.lucene.util.CharsRef;
import org.apache.lucene.util.UnicodeUtil;
import org.apache.solr.common.SolrException;
import org.apache.solr.common.util.DateUtil;
import org.apache.solr.request.SolrQueryRequest;
import org.apache.solr.response.TextResponseWriter;
import org.apache.solr.search.QParser;
import org.apache.solr.util.DateMathParser;

import java.io.IOException;
import java.text.*;
import java.util.*;

// TODO: make a FlexibleDateField that can accept dates in multiple
// formats, better for human entered dates.

// TODO: make a DayField that only stores the day?

/**
 * FieldType that can represent any Date/Time with millisecond precision.
 * <p>
 * Date Format for the XML, incoming and outgoing:
 * </p>
 * <blockquote> A date field shall be of the form 1995-12-31T23:59:59Z The
 * trailing "Z" designates UTC time and is mandatory (See below for an
 * explanation of UTC). Optional fractional seconds are allowed, as long as they
 * do not end in a trailing 0 (but any precision beyond milliseconds will be
 * ignored). All other parts are mandatory. </blockquote>
 * <p>
 * This format was derived to be standards compliant (ISO 8601) and is a more
 * restricted form of the <a
 * href="http://www.w3.org/TR/xmlschema-2/#dateTime-canonical-representation"
 * >canonical representation of dateTime</a> from XML schema part 2. Examples...
 * </p>
 * <ul>
 * <li>1995-12-31T23:59:59Z</li>
 * <li>1995-12-31T23:59:59.9Z</li>
 * <li>1995-12-31T23:59:59.99Z</li>
 * <li>1995-12-31T23:59:59.999Z</li>
 * </ul>
 * <p>
 * Note that DateField is lenient with regards to parsing fractional seconds
 * that end in trailing zeros and will ensure that those values are indexed in
 * the correct canonical format.
 * </p>
 * <p>
 * This FieldType also supports incoming "Date Math" strings for computing
 * values by adding/rounding internals of time relative either an explicit
 * datetime (in the format specified above) or the literal string "NOW", ie:
 * "NOW+1YEAR", "NOW/DAY", "1995-12-31T23:59:59.999Z+5MINUTES", etc... -- see
 * {@link DateMathParser} for more examples.
 * </p>
 * 
 * <p>
 * Explanation of "UTC"...
 * </p>
 * <blockquote> "In 1970 the Coordinated Universal Time system was devised by an
 * international advisory group of technical experts within the International
 * Telecommunication Union (ITU). The ITU felt it was best to designate a single
 * abbreviation for use in all languages in order to minimize confusion. Since
 * unanimous agreement could not be achieved on using either the English word
 * order, CUT, or the French word order, TUC, the acronym UTC was chosen as a
 * compromise." </blockquote>
 * 
 * 
 * @see <a href="http://www.w3.org/TR/xmlschema-2/#dateTime">XML schema part
 *      2</a>
 * 
 */
public class DateField extends PrimitiveFieldType {

	public static TimeZone UTC = TimeZone.getTimeZone("UTC");

	/**
	 * No longer used
	 * 
	 * @deprecated use DateMathParser.DEFAULT_MATH_TZ
	 * @see DateMathParser#DEFAULT_MATH_TZ
	 */
	protected static final TimeZone MATH_TZ = DateMathParser.DEFAULT_MATH_TZ;
	/**
	 * No longer used
	 * 
	 * @deprecated use DateMathParser.DEFAULT_MATH_LOCALE
	 * @see DateMathParser#DEFAULT_MATH_LOCALE
	 */
	protected static final Locale MATH_LOCALE = DateMathParser.DEFAULT_MATH_LOCALE;

	/**
	 * Fixed TimeZone (UTC) needed for parsing/formating Dates in the canonical
	 * representation.
	 */
	protected static final TimeZone CANONICAL_TZ = UTC;
	/**
	 * Fixed Locale needed for parsing/formating Milliseconds in the canonical
	 * representation.
	 */
	protected static final Locale CANONICAL_LOCALE = Locale.ROOT;

	// The XML (external) date format will sort correctly, except if
	// fractions of seconds are present (because '.' is lower than 'Z').
	// The easiest fix is to simply remove the 'Z' for the internal
	// format.

	protected static String NOW = "NOW";
	protected static char Z = 'Z';
	private static char[] Z_ARRAY = new char[] { Z };

	@Override
	public String toInternal(String val) {
		return toInternal(parseMath(null, val));
	}

	/**
	 * Parses a String which may be a date (in the standard format) followed by
	 * an optional math expression.
	 * 
	 * @param now
	 *            an optional fixed date to use as "NOW" in the DateMathParser
	 * @param val
	 *            the string to parse
	 */
	public Date parseMath(Date now, String val) {
		String math = null;
		final DateMathParser p = new DateMathParser();

		if (null != now)
			p.setNow(now);

		if (val.startsWith(NOW)) {
			math = val.substring(NOW.length());
		} else {
			final int zz = val.indexOf(Z);
			if (0 < zz) {
				math = val.substring(zz + 1);
				try {
					// p.setNow(toObject(val.substring(0,zz)));
					p.setNow(parseDate(val.substring(0, zz + 1)));
				} catch (ParseException e) {
					throw new SolrException(
							SolrException.ErrorCode.BAD_REQUEST,
							"Invalid Date in Date Math String:'" + val + '\'',
							e);
				}
			} else {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Invalid Date String:'" + val + '\'');
			}
		}

		if (null == math || math.equals("")) {
			return p.getNow();
		}

		try {
			return p.parseMath(math);
		} catch (ParseException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Invalid Date Math String:'" + val + '\'', e);
		}
	}

	public IndexableField createField(SchemaField field, Object value,
			float boost) {
		// Convert to a string before indexing
		if (value instanceof Date) {
			value = toInternal((Date) value) + Z;
		}
		return super.createField(field, value, boost);
	}

	public String toInternal(Date val) {
		return formatDate(val);
	}

	@Override
	public String indexedToReadable(String indexedForm) {
		return indexedForm + Z;
	}

	@Override
	public CharsRef indexedToReadable(BytesRef input, CharsRef charsRef) {
		UnicodeUtil.UTF8toUTF16(input, charsRef);
		charsRef.append(Z_ARRAY, 0, 1);
		return charsRef;
	}

	@Override
	public String toExternal(IndexableField f) {
		return indexedToReadable(f.stringValue());
	}

	public Date toObject(String indexedForm) throws java.text.ParseException {
		return parseDate(indexedToReadable(indexedForm));
	}

	@Override
	public Date toObject(IndexableField f) {
		try {
			return parseDate(toExternal(f));
		} catch (ParseException ex) {
			throw new RuntimeException(ex);
		}
	}

	@Override
	public SortField getSortField(SchemaField field, boolean reverse) {
		return getStringSort(field, reverse);
	}

	@Override
	public void write(TextResponseWriter writer, String name, IndexableField f)
			throws IOException {
		writer.writeDate(name, toExternal(f));
	}

	/**
	 * Returns a formatter that can be use by the current thread if needed to
	 * convert Date objects to the Internal representation.
	 * 
	 * Only the <tt>format(Date)</tt> can be used safely.
	 * 
	 * @deprecated - use formatDate(Date) instead
	 */
	@Deprecated
	protected DateFormat getThreadLocalDateFormat() {
		return fmtThreadLocal.get();
	}

	/**
	 * Thread safe method that can be used by subclasses to format a Date using
	 * the Internal representation.
	 */
	protected String formatDate(Date d) {
		return fmtThreadLocal.get().format(d);
	}

	/**
	 * Return the standard human readable form of the date
	 */
	public static String formatExternal(Date d) {
		return fmtThreadLocal.get().format(d) + 'Z';
	}

	/**
	 * @see #formatExternal
	 */
	public String toExternal(Date d) {
		return formatExternal(d);
	}

	/**
	 * Thread safe method that can be used by subclasses to parse a Date that is
	 * already in the internal representation
	 */
	public static Date parseDate(String s) throws ParseException {
		return fmtThreadLocal.get().parse(s);
	}

	/**
	 * Parse a date string in the standard format, or any supported by
	 * DateUtil.parseDate
	 */
	public Date parseDateLenient(String s, SolrQueryRequest req)
			throws ParseException {
		// request could define timezone in the future
		try {
			return fmtThreadLocal.get().parse(s);
		} catch (Exception e) {
			return DateUtil.parseDate(s);
		}
	}

	/**
	 * Parses a String which may be a date followed by an optional math
	 * expression.
	 * 
	 * @param now
	 *            an optional fixed date to use as "NOW" in the DateMathParser
	 * @param val
	 *            the string to parse
	 */
	public Date parseMathLenient(Date now, String val, SolrQueryRequest req) {
		String math = null;
		final DateMathParser p = new DateMathParser();

		if (null != now)
			p.setNow(now);

		if (val.startsWith(NOW)) {
			math = val.substring(NOW.length());
		} else {
			final int zz = val.indexOf(Z);
			if (0 < zz) {
				math = val.substring(zz + 1);
				try {
					// p.setNow(toObject(val.substring(0,zz)));
					p.setNow(parseDateLenient(val.substring(0, zz + 1), req));
				} catch (ParseException e) {
					throw new SolrException(
							SolrException.ErrorCode.BAD_REQUEST,
							"Invalid Date in Date Math String:'" + val + '\'',
							e);
				}
			} else {
				throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
						"Invalid Date String:'" + val + '\'');
			}
		}

		if (null == math || math.equals("")) {
			return p.getNow();
		}

		try {
			return p.parseMath(math);
		} catch (ParseException e) {
			throw new SolrException(SolrException.ErrorCode.BAD_REQUEST,
					"Invalid Date Math String:'" + val + '\'', e);
		}
	}

	/**
	 * Thread safe DateFormat that can <b>format</b> in the canonical ISO8601
	 * date format, not including the trailing "Z" (since it is left off in the
	 * internal indexed values)
	 */
	private final static ThreadLocalDateFormat fmtThreadLocal = new ThreadLocalDateFormat(
			new ISO8601CanonicalDateFormat());

	private static class ISO8601CanonicalDateFormat extends SimpleDateFormat {

		protected NumberFormat millisParser = NumberFormat
				.getIntegerInstance(CANONICAL_LOCALE);

		protected NumberFormat millisFormat = new DecimalFormat(".###",
				new DecimalFormatSymbols(CANONICAL_LOCALE));

		public ISO8601CanonicalDateFormat() {
			super("yyyy-MM-dd'T'HH:mm:ss", CANONICAL_LOCALE);
			this.setTimeZone(CANONICAL_TZ);
		}

		@Override
		public Date parse(String i, ParsePosition p) {
			/* delegate to SimpleDateFormat for easy stuff */
			Date d = super.parse(i, p);
			int milliIndex = p.getIndex();
			/* worry about the milliseconds ourselves */
			if (null != d && -1 == p.getErrorIndex()
					&& milliIndex + 1 < i.length()
					&& '.' == i.charAt(milliIndex)) {
				p.setIndex(++milliIndex); // NOTE: ++ to chomp '.'
				Number millis = millisParser.parse(i, p);
				if (-1 == p.getErrorIndex()) {
					int endIndex = p.getIndex();
					d = new Date(d.getTime()
							+ (long) (millis.doubleValue() * Math.pow(10,
									(3 - endIndex + milliIndex))));
				}
			}
			return d;
		}

		@Override
		public StringBuffer format(Date d, StringBuffer toAppendTo,
				FieldPosition pos) {
			/* delegate to SimpleDateFormat for easy stuff */
			super.format(d, toAppendTo, pos);
			/* worry aboutthe milliseconds ourselves */
			long millis = d.getTime() % 1000l;
			if (0L == millis) {
				return toAppendTo;
			}
			if (millis < 0L) {
				// original date was prior to epoch
				millis += 1000L;
			}
			int posBegin = toAppendTo.length();
			toAppendTo.append(millisFormat.format(millis / 1000d));
			if (DateFormat.MILLISECOND_FIELD == pos.getField()) {
				pos.setBeginIndex(posBegin);
				pos.setEndIndex(toAppendTo.length());
			}
			return toAppendTo;
		}

		@Override
		public DateFormat clone() {
			ISO8601CanonicalDateFormat c = (ISO8601CanonicalDateFormat) super
					.clone();
			c.millisParser = NumberFormat.getIntegerInstance(CANONICAL_LOCALE);
			c.millisFormat = new DecimalFormat(".###",
					new DecimalFormatSymbols(CANONICAL_LOCALE));
			return c;
		}
	}

	private static class ThreadLocalDateFormat extends ThreadLocal<DateFormat> {
		DateFormat proto;

		public ThreadLocalDateFormat(DateFormat d) {
			super();
			proto = d;
		}

		@Override
		protected DateFormat initialValue() {
			return (DateFormat) proto.clone();
		}
	}

	@Override
	public ValueSource getValueSource(SchemaField field, QParser parser) {
		field.checkFieldCacheSource(parser);
		return new DateFieldSource(field.getName(), field.getType());
	}

	/** DateField specific range query */
	public Query getRangeQuery(QParser parser, SchemaField sf, Date part1,
			Date part2, boolean minInclusive, boolean maxInclusive) {
		return TermRangeQuery.newStringRange(sf.getName(), part1 == null ? null
				: toInternal(part1), part2 == null ? null : toInternal(part2),
				minInclusive, maxInclusive);
	}

}

class DateFieldSource extends FieldCacheSource {
	// NOTE: this is bad for serialization... but we currently need the
	// fieldType for toInternal()
	FieldType ft;

	public DateFieldSource(String name, FieldType ft) {
		super(name);
		this.ft = ft;
	}

	@Override
	public String description() {
		return "date(" + field + ')';
	}

	@Override
	public FunctionValues getValues(Map context,
			AtomicReaderContext readerContext) throws IOException {
		return new DocTermsIndexDocValues(this, readerContext, field) {
			@Override
			protected String toTerm(String readableValue) {
				// needed for frange queries to work properly
				return ft.toInternal(readableValue);
			}

			@Override
			public float floatVal(int doc) {
				return (float) intVal(doc);
			}

			@Override
			public int intVal(int doc) {
				int ord = termsIndex.getOrd(doc);
				return ord;
			}

			@Override
			public long longVal(int doc) {
				return (long) intVal(doc);
			}

			@Override
			public double doubleVal(int doc) {
				return (double) intVal(doc);
			}

			@Override
			public String strVal(int doc) {
				int ord = termsIndex.getOrd(doc);
				if (ord == 0) {
					return null;
				} else {
					final BytesRef br = termsIndex.lookup(ord, spare);
					return ft.indexedToReadable(br, spareChars).toString();
				}
			}

			@Override
			public Object objectVal(int doc) {
				int ord = termsIndex.getOrd(doc);
				if (ord == 0) {
					return null;
				} else {
					final BytesRef br = termsIndex.lookup(ord, new BytesRef());
					return ft.toObject(null, br);
				}
			}

			@Override
			public String toString(int doc) {
				return description() + '=' + intVal(doc);
			}
		};
	}

	@Override
	public boolean equals(Object o) {
		return o instanceof DateFieldSource && super.equals(o);
	}

	private static int hcode = DateFieldSource.class.hashCode();

	@Override
	public int hashCode() {
		return hcode + super.hashCode();
	};
}
